package game;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.Map;

/**
 * <code>PlayerIOManager</code> is responsible for communication with
 * {@link Player}.
 * <p>
 * All messages for interaction with player are saved as constants.
 * 
 * @author Tsvetan Vasilev
 * 
 */
public class PlayerIOManager {

	// the class a
	private Socket playerSocket;
	private BufferedReader socketReader;
	private PrintWriter socketWriter;

	/**
	 * The count of specific Telnet's characters which are added automatically
	 * in the first console input.
	 * <p>
	 * The count of characters is found with "trial and error" method.
	 */
	private static int TELNET_SPECIFIC_ENCODING = 21;

	// message constants for console communication with the player
	private String SECRET_NUMBER_MESSAGE = "Please insert your number: ";
	private String OPPONENT_NUMBER_MESSAGE = "Please make your guess: ";
	private String PLAYER_NAME_INPUT_MESSAGE = "Please enter your name"
			+ " (the name should contain 2 or more characters): ";
	private String FREE_PLAYERS_LIST = "\nFree Players:";
	private String WAITING_CHOICE = "0)(wait for another player to choose you)";
	private String CHOOSE_PLAYER_MESSAGE = "Choose player: ";
	private String PADDING = "            ";
	private String PADDING_FIRST_ITERATION = PADDING + PADDING + "          ";
	// opponentName,opponentGuessNmbr,bullsNcows
	private String PLAYER_GUESS = PADDING + "Your Guess: %s  %s";
	private String OPPONENT_GUESS = PADDING + "%s's Guess: %s  %s";
	private String WINNER_MESSAGE = "Game end. You win!";
	private String LOSER_MESSAGE = "Game end. Winner - %s.";
	private String GAME_STARTED_MESSAGE = "\n%s vs %s game started.";
	private String ASK_NEW_GAME_MESSAGE = "Do you want to play new game?  y/n:  ";

	// --- Constructors,Getters,Setters ----------------------------------------
	/**
	 * Accepts <code>a Socket</code> and provides <b>high-level</b>
	 * <code>Reader</code> and <code>Writer</code> to facilitate communication
	 * processing.
	 * 
	 * @param playerSocket
	 *            accepted by {@link Server}
	 */
	public PlayerIOManager(Socket playerSocket) throws IOException {
		this.playerSocket = playerSocket;
		socketReader = new BufferedReader(new InputStreamReader(
				playerSocket.getInputStream()));
		socketWriter = new PrintWriter(new OutputStreamWriter(
				playerSocket.getOutputStream()));
	}

	public PrintWriter getPlayerWriter() {
		return socketWriter;
	}

	public BufferedReader getPlayerReader() {
		return socketReader;
	}

	// --- Methods -------------------------------------------------------------

	/**
	 * Outputs the list with all free players' names and also a choice to wait
	 * 
	 * @param id2NamesFreePlayers
	 *            free players' names
	 */
	public void offerPlayers(Map<Integer, String> id2NamesFreePlayers) {
		getPlayerWriter().println(FREE_PLAYERS_LIST);
		getPlayerWriter().println(WAITING_CHOICE);

		int counter = 1;
		for (Map.Entry<Integer, String> entry : id2NamesFreePlayers.entrySet()) {
			getPlayerWriter().println(counter + ") " + entry.getValue());
			counter++;
		}
		getPlayerWriter().flush();
	}

	/**
	 * Accepts and checks chosen opponent player's index
	 * 
	 * @param player
	 *            current player
	 * @param freePlayersCount
	 * @return chosen Opponent Player's index
	 * @throws IOException
	 */
	public int chooseOpponentPlayerIndex(Player player, int freePlayersCount)
			throws IOException {
		String chosenIndex = null;
		do {
			getPlayerWriter().print(CHOOSE_PLAYER_MESSAGE);
			getPlayerWriter().flush();
			chosenIndex = getPlayerReader().readLine();
		} while (!isCorrectChosenPlayerIndex(chosenIndex, freePlayersCount));

		return Integer.parseInt(chosenIndex);
	}

	/**
	 * Outputs message when a new game is started
	 * 
	 * @param playerName
	 * @param opponentName
	 */
	public void notifyForGameStart(String playerName, String opponentName) {
		String message = String.format(GAME_STARTED_MESSAGE, playerName,
				opponentName);
		this.getPlayerWriter().println(message);
		this.getPlayerWriter().flush();
	}

	/**
	 * Accepts and check player's secret number
	 */
	public String chooseSecretNumber() throws IOException {
		return enterNumber(SECRET_NUMBER_MESSAGE);
	}

	/**
	 * Accepts and check player's guess during the game
	 * 
	 * @return
	 * @throws IOException
	 */
	public String guessOpponentNumber() throws IOException {
		return enterNumber(OPPONENT_NUMBER_MESSAGE);
	}

	/**
	 * Accepts chosen player's name when user connects to Telnet input
	 * <p>
	 * There is a special case with first Telnet's console input.
	 * 
	 * @return player's name
	 * @throws IOException
	 */
	public String getPlayerName() throws IOException {
		String playerName = new String();
		do {
			getPlayerWriter().println(PLAYER_NAME_INPUT_MESSAGE);
			getPlayerWriter().flush();
			playerName = getPlayerReader().readLine();
			// because of telnet's foolish encoding magic
			playerName = decodeTelnetInput(playerName);
		} while (!isCorrectPlayerName(playerName));

		return decodeTelnetInput(playerName);
	}

	/**
	 * Informs player about the count of <code>bulls</code> and
	 * <code>cows</code> in his/her current guess.
	 * 
	 * @param guessNumber
	 * @param bullsNcows
	 */
	public void notifyInterimGameState(String guessNumber, String bullsNcows) {
		String message = String.format(PLAYER_GUESS, guessNumber, bullsNcows);
		getPlayerWriter().print(message);
		getPlayerWriter().flush();
	}

	/**
	 * Informs player about the opponent's guess result
	 * 
	 * @param opponentName
	 * @param opponentGuessNmbr
	 * @param bullsNcows
	 */
	public void notifyForOpponentMove(String opponentName,
			String opponentGuessNmbr, String bullsNcows) {
		String message = String.format(OPPONENT_GUESS, opponentName,
				opponentGuessNmbr, bullsNcows);
		getPlayerWriter().println(message);
		getPlayerWriter().flush();
	}

	/**
	 * Sends message to the winner in the game
	 */
	public void announceWinner() {
		getPlayerWriter().println();
		getPlayerWriter().println(WINNER_MESSAGE);
		getPlayerWriter().flush();
	}

	/**
	 * Sends message to the loser in the game
	 * 
	 * @param playyer
	 *            winner
	 */
	public void announceWinner(Player player) {
		String message = String.format(LOSER_MESSAGE, player.getName());
		getPlayerWriter().println(message);
		getPlayerWriter().flush();
	}

	/**
	 * Asks player to play new game
	 */
	public boolean askForNewGame() throws IOException {
		String newGameChoice = null;
		do {
			getPlayerWriter().print(ASK_NEW_GAME_MESSAGE);
			getPlayerWriter().flush();
			newGameChoice = getPlayerReader().readLine();
		} while (!isCorrectCharacterInput(newGameChoice));

		return ('y' == newGameChoice.charAt(0)) ? true : false;
	}

	public void passPaddingFirstIteration() {
		getPlayerWriter().print(PADDING_FIRST_ITERATION);
		getPlayerWriter().flush();

	}

	/**
	 * Checks if the player is connected
	 * 
	 * @return player's connection status
	 * @throws SocketException
	 */
	public boolean isConnected() throws SocketException {
		if (playerSocket == null || !playerSocket.isConnected()) {
			if (playerSocket == null || !playerSocket.getKeepAlive()) {
				return false;
			}
			return false;
		}
		return true;
	}

	/**
	 * Closes used communication resources
	 * 
	 * @throws IOException
	 */
	public void closeConnection() throws IOException {
		if (socketReader != null) {
			socketReader.close();
		}
		if (socketWriter != null) {
			socketWriter.close();
		}
		if (playerSocket != null) {
			playerSocket.close();
		}
		playerSocket = null;
	}

	// --- Auxiliary Methods ---------------------------------------------------

	private boolean isCorrectPlayerName(String playerName) {
		if (playerName == null || playerName.length() < 2) {
			return false;
		}
		return true;
	}

	/**
	 * Cuts Telnet's system characters from the first inputed string in Telnet
	 * console.
	 * <p>
	 * If only 1 character is inputed, the console doesn't accept "Enter"-key as
	 * end of input. So an additional rule for playerName's length(>=2
	 * characters) is added.
	 * 
	 * @param name
	 *            first inputed string in the Telnet console
	 * @return correct string with eventually cutted Telnet's system characters
	 */
	private String decodeTelnetInput(String string) {
		if (string != null && string.length() >= TELNET_SPECIFIC_ENCODING) {
			return string.substring(TELNET_SPECIFIC_ENCODING, string.length());
		}
		return string;
	}

	private boolean isCorrectCharacterInput(String newGameChoice) {
		if (newGameChoice == null || newGameChoice.length() != 1) {
			return false;
		}
		char choice = newGameChoice.charAt(0);
		return (choice == 'y' || choice == 'n') ? true : false;
	}

	private String enterNumber(String message) throws IOException {
		String secretNumber = null;
		do {
			getPlayerWriter().print(message);
			getPlayerWriter().flush();
			secretNumber = getPlayerReader().readLine();
		} while (!isCorrectEnteredNumber(secretNumber));
		return secretNumber;
	}

	private static boolean isCorrectEnteredNumber(String number) {
		if (number == null || number.length() != 4) {
			return false;
		}
		if ('0' == number.charAt(0)) {
			return false;
		}
		for (int i = 0; i < number.length(); i++) {
			if (!Character.isDigit(number.charAt(i))) {
				return false;
			}
			for (int j = 0; j < number.length(); j++) {
				if (i == j) {
					continue;
				}
				if (number.charAt(i) == number.charAt(j)) {
					return false;
				}
			}
		}
		return true;
	}

	private boolean isCorrectChosenPlayerIndex(String chosenIndex,
			int freePlayersCount) {
		if (chosenIndex == null || chosenIndex.isEmpty()) {
			return false;
		}
		int index = 0;
		try {
			index = Integer.parseInt(chosenIndex);
		} catch (NumberFormatException nfe) {
			return false;
		}
		if (index < 0 || index > freePlayersCount) {
			return false;
		}
		return true;
	}

}
